package com.jsftoolkit.utils.xmlpull;

import java.io.IOException;
import java.io.StringReader;
import java.util.Iterator;
import java.util.LinkedList;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

import com.jsftoolkit.utils.Utils;

/**
 * This {@link PullEventSource} has a few unusual properties. Firstly, it only
 * operates on text strings. Secondly, the given text string may have multiple
 * root elements and contain text outside of elements.
 * <p>
 * This class encloses the passed in text in an wrapper element before parsing
 * so that the document will be considered well formed.
 * <p>
 * Consecutive calls to {@link #characters(char[], int, int)} will be
 * consolidated into one {@link BodyText} event.
 * <p>
 * This class is intended for small XML fragments.
 * <p>
 * Generated Events: {@link StartElement}, {@link AttributeEvent},
 * {@link BodyText}, {@link EndElement}.
 * 
 * @author noah
 * 
 */
public class PullEventCollector extends DefaultHandler implements
		ContentHandler, PullEventSource {

	/**
	 * Wrapper element to make the passed in text well formed.
	 */
	private static final String WRAPPER = "PullEventCollectorWrapper";

	private static final XMLReader READER;

	static {
		String[] toTry = { "org.apache.xerces.parsers.SAXParser",
				"org.apache.crimson.parser.XMLReaderImpl" };
		int i = 0;
		XMLReader reader = null;
		while (reader == null && i < toTry.length) {
			try {
				reader = XMLReaderFactory.createXMLReader(toTry[i++]);
			} catch (SAXException e) {
			}
		}
		if (reader == null) {
			try {
				reader = XMLReaderFactory.createXMLReader();
			} catch (SAXException e) {
				// not anything we can do if we can't create an XML Reader
				throw new RuntimeException(e);
			}
		}
		READER = reader;
	}

	protected final LinkedList<PullEvent> events = new LinkedList<PullEvent>();

	/**
	 * Parses the given text for events.
	 * 
	 * @param text
	 * @throws IOException
	 * @throws SAXException
	 */
	public PullEventCollector(String text) throws IOException, SAXException {
		this(READER, text);
	}

	/**
	 * Constructs a new instance that uses the given reader.
	 * 
	 * @param reader
	 * @param text
	 * @throws IOException
	 * @throws SAXException
	 */
	public PullEventCollector(XMLReader reader, String text)
			throws IOException, SAXException {
		reader.setContentHandler(this);
		reader.parse(new InputSource(new StringReader(String.format(
				"<%s>%s</%1$s>", WRAPPER, Utils.toString(text)))));
	}

	@Override
	public void startElement(String uri, String localName, String qName,
			Attributes attributes) {
		if (!WRAPPER.equals(qName)) {
			events.offer(new StartElement(qName));
			for (int i = 0; i < attributes.getLength(); i++) {
				events.offer(new AttributeEvent(attributes.getQName(i),
						attributes.getValue(i)));
			}
		}
	}

	@Override
	public void characters(char[] ch, int start, int length) {
		if (isBodyText()) {
			((BodyText) events.getLast()).getText().append(ch, start, length);
		} else {
			events.offer(new BodyText(ch, start, length));
		}
	}

	protected boolean isBodyText() {
		return !events.isEmpty() && events.getLast() instanceof BodyText;
	}

	@Override
	public void endElement(String uri, String localName, String qName) {
		if (!WRAPPER.equals(qName)) {
			events.offer(new EndElement(qName));
		}
	}

	public Iterator<PullEvent> iterator() {
		return events.iterator();
	}

}
